﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace T4UtilityBelt.Serializers
{
    public class XSerializer //<T>
        //where T : class
    {

        #region Deserialize

        public T Deserialize<T>(string document) where T : class
        {
            return Deserialize<T>(XDocument.Parse(document));
        }

        public T Deserialize<T>(XDocument document) where T : class
        {
            return CreateFromFirstElement(document.Root) as T;
        }

        private string _xmlns;

        private object CreateFromFirstElement(XElement xel)
        {
            var x = xel.Attribute("xmlns");
            if (x != null)
            {
                _xmlns = x.Value;
            }
            return CreateFromElement(xel);
        }

        private object CreateFromElement(XElement xel)
        {
            string ns = xel.Name.NamespaceName;
            string localName = xel.Name.LocalName;
            if (string.IsNullOrEmpty(ns))
            {
                ns = _xmlns;
            }
            Type t = Type.GetType(ns + "." + localName);

            if (t == null)
            {
                throw new SerializerException(string.Format("Type not found! Namespace: {0} TypeName: {1}", ns,
                                                            localName));
            }

            object value = Activator.CreateInstance(t);

            FillFromAttributes(xel, value);

            FillFromElements(xel, value);

            return value;

            //throw new SerializerException(string.Format("Invalid namespace. Namespace: {0} TypeName: {1}", ns,
            //                                            localName));
        }

        private static void FillFromAttributes(XElement xel, object value)
        {
            Type t = value.GetType();
            if (!xel.HasAttributes) 
                return;
            foreach (var a in xel.Attributes())
            {
                if (!IsValidAttributeName(a.Name)) 
                    continue;
                PropertyInfo pi = t.GetProperty(a.Name.ToString());
                if (pi == null)
                {
                    throw new SerializerException(string.Format(
                        "Attribute not found. Attribute: {0} TypeName: {1}", a.Name,
                        t.FullName));
                }
                if (typeof (IXAttributeSerializer).IsAssignableFrom(pi.PropertyType))
                {
                    IXAttributeSerializer x = Activator.CreateInstance(pi.PropertyType) as IXAttributeSerializer;
                    if (x != null)
                    {
                        x.Deserialize(a.Value);
                    }
                    pi.SetValue(value, x);
                }
                else
                {
                    try
                    {
                        object convertedValue = Convert.ChangeType(a.Value, pi.PropertyType);
                        pi.SetValue(value, convertedValue);
                    }
                    catch (Exception ex)
                    {
                        throw new SerializerException(string.Format(
                            "Property cannot be set for. Attribute: {0} TypeName: {1}, Value:{3} Message: {2}",
                            a.Name,
                            t.FullName, ex.Message, value), ex);
                    }
                }
            }
        }

        private void FillFromElements(XElement xel, object value)
        {
            Type t = value.GetType();
            if (!xel.HasElements) 
                return;
            foreach (var child in xel.Elements())
            {
                PropertyInfo pi = t.GetProperty(child.Name.LocalName, BindingFlags.Public | BindingFlags.Instance);
                if (pi == null)
                {
                    pi = GetDefaultProperty(t);
                    if (pi == null)
                    {

                        throw new SerializationException(string.Format("Property not found for: {0}, {1} ", xel.Name,
                                                                       child.Name));
                    }

                    IList list = pi.GetValue(value) as IList;
                    if (list != null)
                    {
                        object obj = CreateFromElement(child);
                        list.Add(obj);
                        //pi.SetValue(value, obj);
                    }
                    else
                    {
                        FillFromElement(pi, child, value);
                    }
                }
                else
                {
                    IList list = pi.GetValue(value) as IList;
                    if (list != null)
                    {
                        FillListFromElements(list, child);
                    }
                    else
                    {
                        FillFromElement(pi, child, value);
                    }
                }
            }
        }

        private static PropertyInfo GetDefaultProperty(Type t)
        {
            XDefaultPropertyAttribute defaultProperty = t.GetCustomAttribute<XDefaultPropertyAttribute>();
            if (defaultProperty == null)
            {
                return null;
            }
            PropertyInfo pi = t.GetProperty(defaultProperty.Property);
            if (pi == null)
            {

                throw new SerializationException(string.Format("Property not found for default property: {0}, {1} ", t.FullName,
                                                               defaultProperty.Property));
            }
            return pi;
        }

        private void FillFromElement(PropertyInfo pi, XElement xel, object parentValue)
        {
            if (!xel.HasElements)
            {
                return;
            }
            bool first = true;
            foreach (var child in xel.Elements())
            {
                if (!first)
                {
                    throw new SerializerException(string.Format(
                        "More then one element defined for: {0}", xel.Name));
                }
                first = false;
                object value = CreateFromElement(child);
                if (value != null)
                {
                    try
                    {
                        pi.SetValue(parentValue, value);
                    }
                    catch (Exception ex)
                    {
                        throw new SerializerException(string.Format(
                            "Attribute not found. Name: {0} Type: {1} Message:{2}", pi.Name, pi.ReflectedType.FullName, ex.Message));
                    }
                }
            }
        }

        private void FillListFromElements(IList list, XElement xel)
        {
            foreach (var child in xel.Elements())
            {
                object value = CreateFromElement(child);
                if (value != null)
                {
                    list.Add(value);
                }
                else
                {
                    throw new SerializerException(string.Format(
                        "Attribute not found. TypeName: {0}", child.Name));
                }
            }
        }

        private static bool IsValidAttributeName(XName name)
        {
            if (name.LocalName == "xmlns")
            {
                return false;
            }
            return XNamespace.Xmlns != name.Namespace;
        }

        #endregion

        #region Serialize

        private Dictionary<string, Xns> _Namespaces = new Dictionary<string, Xns>();

        private class Xns
        {
            public string ns { get; set; }
            public XNamespace Namespace { get; set; }
        }

        public string Serialize<T>(T obj) where T : class
        {
            XDocument doc;
            XElement xel = CreateXElement(null, obj);
            if (xel == null)
            {
                doc = new XDocument();
            }
            else
            {
                foreach (var xnse in _Namespaces)
                {
                    if (xnse.Value.ns == null)
                    {
                        //xel.Add(new XAttribute(XNamespace.Xmlns + "", xnse.Key));
                    }
                    else
                    {
                        xel.Add(new XAttribute(XNamespace.Xmlns + xnse.Value.ns, xnse.Key));
                    }
                }
                doc = new XDocument(xel);
            }
            return doc.ToString();
        }

        private Xns CreateNamespace(Type t)
        {
            if (t.Namespace == null)
            {
                return null;
            }
            if (_Namespaces.ContainsKey(t.Namespace))
            {
                Xns xns = _Namespaces[t.Namespace];
                if (string.IsNullOrEmpty(xns.ns))
                {
                    return null;
                }
                return _Namespaces[t.Namespace];
            }
            Xns x;
            if (_Namespaces.Count == 0)
            {
                x = new Xns()
                {
                    Namespace = t.Namespace,
                    //ns = "p" + _Namespaces.Count
                };
                _Namespaces.Add(t.Namespace, x);
                return x;
            }
            else
            {
                x = new Xns()
                {
                    Namespace = t.Namespace,
                    ns = "p" + _Namespaces.Count
                };
                _Namespaces.Add(t.Namespace, x);
                return x;
            }


        }

        private XElement CreateXElement(PropertyInfo propertyInfo, object obj)
        {
            if (obj == null)
                return null;

            IXShouldSerialize shouldSerialize = obj as IXShouldSerialize;
            if (shouldSerialize != null
                && !shouldSerialize.ShouldSerialize)
            {
                return null;
            }

            Type t = obj.GetType();

            try
            {
                Xns xns = CreateNamespace(t);

                XElement xel;

                if (xns == null)
                {
                    xel = new XElement(t.Name);
                }
                else
                {
                    xel = new XElement(xns.Namespace + t.Name);
                }

                XElement xContainer;
                if (propertyInfo != null)
                {
                    xContainer = new XElement(propertyInfo.Name);
                    xContainer.Add(xel);
                }
                else
                {
                    xContainer = xel;
                }

                AddProperties(obj, t, xel);

                return xContainer;
            }
            catch (Exception x)
            {
                throw new SerializationException("Error XElement with: " + t.FullName + " " + x.Message, x);
            }
        }

        private static XAttribute CreateXAttribute(PropertyInfo propertyInfo, object value)
        {
            if (value == null)
            {
                return null;
            }

            try
            {
                XAttribute xab = new XAttribute(propertyInfo.Name, value);

                return xab;
            }
            catch (Exception x)
            {
                throw new SerializerException("Error XAttribute with: " + propertyInfo.Name + " " + x.Message);
            }
        }

        private void AddProperties(object obj, Type t, XElement xel)
        {
            foreach (PropertyInfo propertyInfo in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (!HasXIgnore(propertyInfo)
                    && IsValidPropertyGet(propertyInfo))
                {
                    object value = propertyInfo.GetValue(obj);
                    if (value != null
                        && !HasDefaultValue(propertyInfo, value)
                        && HasSpecifiedValue(propertyInfo, obj))
                    {
                        IXAttributeSerializer xSerializer = value as IXAttributeSerializer;
                        IList values = value as IList;
                        IXDefaultValue iDefault = value as IXDefaultValue;
                        if (iDefault != null
                            && ValueIsDefault(propertyInfo, iDefault))
                        {}
                        else if (values != null)
                        {
                            CreateXElements(propertyInfo, values, xel);
                        }
                        else if (xSerializer != null)
                        {
                            XAttribute xchild = CreateXAttribute(propertyInfo, xSerializer.Serialize());
                            if (xchild != null)
                            {
                                xel.Add(xchild);
                            }
                        }
                        else if (ValueIsDefault(propertyInfo, value))
                        {
                        }
                        else if (!IsValidPropertySet(propertyInfo))
                        {
                        }
                        else if (value is string
                                 || propertyInfo.PropertyType.IsValueType)
                        {
                            XAttribute xchild = CreateXAttribute(propertyInfo, value);
                            if (xchild != null)
                            {
                                xel.Add(xchild);
                            }
                        }
                        else
                        {
                            XElement xchild = CreateXElement(propertyInfo, value);
                            if (xchild != null)
                            {
                                xel.Add(xchild);
                            }
                        }
                    }
                }
            }
        }

        private static bool ValueIsDefault(PropertyInfo propertyInfo, IXDefaultValue value)
        {
            DefaultValueAttribute defaultValue = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();
            if (defaultValue != null)
            {
                return value.CompareDefault(defaultValue.Value);
            }
            return false;
        }

        private static bool ValueIsDefault(PropertyInfo propertyInfo, object value)
        {
            DefaultValueAttribute defaultValue = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();
            if (defaultValue != null)
            {
                return Equals(value, defaultValue.Value);
            }
            return false;
        }

        private static bool IsValidPropertySet(PropertyInfo propertyInfo)
        {
            return (propertyInfo.SetMethod != null
                    && propertyInfo.SetMethod.IsPublic);
        }

        private static bool IsValidPropertyGet(PropertyInfo propertyInfo)
        {
            return (propertyInfo.GetMethod != null
                    && propertyInfo.GetMethod.IsPublic);
        }

        private void CreateXElements(PropertyInfo propertyInfo, IList values, XElement xParent)
        {
            XElement xel = xParent;
            bool first = !IsDefaultProperty(propertyInfo);
            foreach (object value in values)
            {
                if (first)
                {
                    xel = new XElement(propertyInfo.Name);
                    xParent.Add(xel);
                    first = false;
                }
                XElement xchild = CreateXElement(null, value);
                if (xchild != null)
                {
                    xel.Add(xchild);
                }
            }
        }

        private static bool IsDefaultProperty(PropertyInfo propertyInfo)
        {
            var attr = propertyInfo.DeclaringType.GetCustomAttribute<XDefaultPropertyAttribute>();
            if (attr == null)
            {
                return false;
            }
            return attr.Property == propertyInfo.Name;
        }

        private static bool HasXIgnore(PropertyInfo propertyInfo)
        {
            return propertyInfo.GetCustomAttribute<XIgnoreAttribute>() != null;
        }

        private static bool HasSpecifiedValue(PropertyInfo propertyInfo, object value)
        {
            XSpecifiedValueAttribute ignore = propertyInfo.GetCustomAttribute<XSpecifiedValueAttribute>();
            if (ignore != null)
            {
                return ignore.IsValueSpecified(value);
            }
            return true;
        }

        private static bool HasDefaultValue(PropertyInfo propertyInfo, object value)
        {
            DefaultValueAttribute attr = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();
            if (attr == null)
            {
                return false;
            }
            IComparable ic = attr.Value as IComparable;
            if (ic != null)
            {
                return ic.CompareTo(value) == 0;
            }
            IComparer icr = attr.Value as IComparer;
            if (icr != null)
            {
                return icr.Compare(attr.Value, value) == 0;
            }
            return attr.Value == value;
        }

        #endregion

    }
}
